﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Text;
using WebServer.BusinessLogic.Logs;

namespace WebServer.BusinessLogic.Communication
{
    public class SocketManager : IDisposable
    {
        private ISocketWrapper _tcpSocket;

        private IPEndPoint _endPointIp;

        /// <summary>
        /// Handles communicator's TCP connection
        /// </summary>
        /// <param name="socketWrapper">tcpSocket object</param>
        public SocketManager(ISocketWrapper socketWrapper)
        {
            _tcpSocket = socketWrapper;

            _endPointIp = _tcpSocket.RemoteEndPoint as IPEndPoint;
            if (_endPointIp == null)
            {
                // if we don't have user's IP, just exit
                Logger.Instance.Log("Can't determine user's IP");
                HasError = true;
            }
        }

        public bool HasError { get; private set; }

        /// <summary>
        /// Handles request and sends response to browser.
        /// </summary>
        
        public Request GetRequestFromSocket()
        {
            if (HasError)
            {
                return null;
            }

            try
            {
                // log connected user's IP and Port
                Logger.Instance.Log(string.Format("Connected {0}:{1}", _endPointIp.Address, _endPointIp.Port));

                // receive whole data from Socket
                byte[] data = Receive(_tcpSocket);

                // parse into Request
                Request req = new Request();
                req.ParseRequest(data);
                req.UserIpAddress = _endPointIp.Address.ToString();

                // log request
                LogRequest(req);

                if (req.IsCorrect)
                {
                    return req;
                }
            }
            catch (SocketException)
            {
                Logger.Instance.Log("Error in reading data from Socket");
                HasError = true;
            }

            return null;
        }

        /// <summary>
        /// Converts response to bytes.
        /// </summary>
        /// <param name="response">response objects</param>
        /// <returns>bytes of response</returns>
        private byte[] GetResponseBytes(Response response)
        {
            var responseBuilder = new ResponseBuilder(response);
            var responseBytes = responseBuilder.GetResponseBytes();
            if (responseBytes == null)
            {
                //// if response is null, create Server 500 Error:
                var r = new Response();
                r.SetServerError();
                responseBuilder = new ResponseBuilder(r);
                responseBytes = responseBuilder.GetResponseBytes();
            }

            return responseBytes;
        }

        /// <summary>
        /// Receiving data from socket in continious mode
        /// </summary>
        /// <param name="tcpSocket">Socket object</param>
        /// <returns>Bytes of data, read from Socket</returns>
        private byte[] Receive(ISocketWrapper tcpSocket)
        {
            byte[] buffer = new byte[tcpSocket.ReceiveBufferSize]; // buffer for TCP packet
            int receivedBytesCount; // count of received bytes from Socket.Receive
            var pageBytes = new List<byte>(); // resulting list of Bytes
            bool expectedMoreBytes; // indicates that browser didn't send all bytes yet

            do
            {
                Request tempRequest = new Request();

                // receive bytes into buffer and return number of bytes received
                receivedBytesCount = tcpSocket.Receive(buffer.Length, 0);

                // copy from buffer into total container - pageBytes
                var newArray = new byte[receivedBytesCount];
                Array.Copy(tcpSocket.Buffer, newArray, receivedBytesCount);
                pageBytes.AddRange(newArray);

                // parse request to determine if we need more bytes from socket
                tempRequest.ParseRequest(newArray);
                if (tempRequest.ContentLength != tempRequest.Body.Length)
                {
                    expectedMoreBytes = true;
                }
                else
                {
                    expectedMoreBytes = false;
                }
            }
            while (receivedBytesCount == tcpSocket.Buffer.Length || expectedMoreBytes);

            return pageBytes.ToArray();
        }

        /// <summary>
        /// Logs request data into the Logger
        /// </summary>
        /// <param name="req">Request to get information from</param>
        private void LogRequest(Request req)
        {
            // log request
            Logger.Instance.Log(string.Format("{0} {1} from {2}; UserAgent: {3}", req.HttpMethod, req.HttpPath, req.UserIpAddress, req.UserAgent));
            Logger.Instance.Log(
                string.Format(
                    "{0} {1} from {2}; UserAgent: {3}",
                    req.HttpMethod,
                    req.HttpPath,
                    req.UserIpAddress,
                    req.UserAgent));
        }

        /// <summary>
        /// Trying close socket
        /// </summary>
        private void TryClosingSocket()
        {
            try
            {
                if (_tcpSocket.Connected)
                {
                    _tcpSocket.Shutdown(SocketShutdown.Both);
                    _tcpSocket.Close();
                }
            }
            catch (SocketException)
            {
                Trace.WriteLine("forseclosed");
            }
        }

        /// <summary>
        /// Sends response to browser through Socket
        /// </summary>
        /// <param name="response">Reponse, needed to be sending to browser</param>
        public void SendResponse(Response response)
        {
            if (HasError)
                return;

            if (response == null)
            {
                // respond with error message
                response = new Response();
                response.MimeType = "text/html";
                response.Data =
                    Encoding.UTF8.GetBytes("Request was not in correct format, please fix your browser!");
            }

            try
            {
                // form Response
                var responseBytes = this.GetResponseBytes(response);

                // send data back to opened socket
                _tcpSocket.Send(responseBytes);
            }
            catch(SocketException e)
            {
                Logger.Instance.Log("Error while sending data back to browser: " + e.Message);
                HasError = true;
            }
        }

        /// <summary>
        /// Closes opened Socket
        /// </summary>
        public void CloseSocket()
        {
            if (HasError)
                return;

            Logger.Instance.Log(string.Format("Disconnecting {0}:{1}", _endPointIp.Address, _endPointIp.Port));

            // close socket on dispose
            TryClosingSocket();
        }

        /// <summary>
        /// Disposes object by closing socket
        /// </summary>
        public void Dispose()
        {
            CloseSocket();
        }
    }
}